2 位追蹤者

授權

授權是驗證使用者是否有足夠權限執行某些操作的過程。Yii 提供了兩種授權方法:存取控制過濾器 (ACF) 和基於角色的存取控制 (RBAC)。

存取控制過濾器

存取控制過濾器 (ACF) 是一種簡單的授權方法,實作為 yii\filters\AccessControl,最適合只需要一些簡單存取控制的應用程式使用。顧名思義,ACF 是一個動作過濾器,可以用在控制器或模組中。當使用者請求執行一個動作時,ACF 會檢查一組存取規則,以判斷使用者是否被允許存取請求的動作。

以下程式碼顯示如何在 site 控制器中使用 ACF

use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::class,
                'only' => ['login', 'logout', 'signup'],
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['login', 'signup'],
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['logout'],
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }
    // ...
}

在上面的程式碼中,ACF 作為行為附加到 site 控制器。這是使用動作過濾器的典型方式。only 選項指定 ACF 應該僅適用於 loginlogoutsignup 動作。site 控制器中的所有其他動作不受存取控制的約束。rules 選項列出存取規則,其內容如下:

  • 允許所有訪客(尚未驗證身份)使用者存取 loginsignup 動作。roles 選項包含問號 ?,這是一個代表「訪客使用者」的特殊符號。
  • 允許已驗證身份的使用者存取 logout 動作。@ 字元是另一個代表「已驗證身份的使用者」的特殊符號。

ACF 透過從上到下逐一檢查存取規則來執行授權檢查,直到找到符合目前執行環境的規則為止。然後將使用符合規則的 allow 值來判斷使用者是否已獲得授權。如果沒有任何規則符合,則表示使用者未獲得授權,並且 ACF 將停止進一步的動作執行。

當 ACF 判斷使用者未獲得授權存取目前的動作時,預設會採取以下措施:

您可以透過配置 yii\filters\AccessControl::$denyCallback 屬性來自訂此行為,如下所示:

[
    'class' => AccessControl::class,
    ...
    'denyCallback' => function ($rule, $action) {
        throw new \Exception('You are not allowed to access this page');
    }
]

存取規則支援許多選項。以下是支援選項的摘要。您也可以擴展 yii\filters\AccessRule 以建立您自己的自訂存取規則類別。

  • allow:指定這是「允許」還是「拒絕」規則。

  • actions:指定此規則符合哪些動作。這應該是一個動作 ID 陣列。比較是區分大小寫的。如果此選項為空或未設定,則表示該規則適用於所有動作。

  • controllers:指定此規則符合哪些控制器。這應該是一個控制器 ID 陣列。每個控制器 ID 都以模組 ID(如果有的話)作為前綴。比較是區分大小寫的。如果此選項為空或未設定,則表示該規則適用於所有控制器。

  • roles:指定此規則符合哪些使用者角色。識別出兩個特殊角色,並且它們透過 yii\web\User::$isGuest 進行檢查

    • ?:符合訪客使用者(尚未驗證身份)
    • @:符合已驗證身份的使用者

    使用其他角色名稱將觸發 yii\web\User::can() 的調用,這需要啟用 RBAC(將在下一個小節中描述)。如果此選項為空或未設定,則表示此規則適用於所有角色。

  • roleParams:指定將傳遞給 yii\web\User::can() 的參數。請參閱下面描述 RBAC 規則的部分,以了解如何使用它。如果此選項為空或未設定,則不會傳遞任何參數。

  • ips:指定此規則符合哪些用戶端 IP 位址。IP 位址可以在結尾包含萬用字元 *,以便它符合具有相同前綴的 IP 位址。例如,「192.168.*」符合網段「192.168.」中的所有 IP 位址。如果此選項為空或未設定,則表示該規則適用於所有 IP 位址。

  • verbs:指定此規則符合哪些請求方法(例如 GETPOST)。比較是不區分大小寫的。

  • matchCallback:指定應該呼叫的 PHP 可呼叫物件,以判斷是否應套用此規則。

  • denyCallback:指定當此規則將拒絕存取時應該呼叫的 PHP 可呼叫物件。

以下範例顯示如何使用 matchCallback 選項,該選項允許您編寫任意存取檢查邏輯

use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::class,
                'only' => ['special-callback'],
                'rules' => [
                    [
                        'actions' => ['special-callback'],
                        'allow' => true,
                        'matchCallback' => function ($rule, $action) {
                            return date('d-m') === '31-10';
                        }
                    ],
                ],
            ],
        ];
    }

    // Match callback called! This page can be accessed only each October 31st
    public function actionSpecialCallback()
    {
        return $this->render('happy-halloween');
    }
}

基於角色的存取控制 (RBAC)

基於角色的存取控制 (RBAC) 提供了一種簡單但功能強大的集中式存取控制。有關 RBAC 與其他更傳統的存取控制方案比較的詳細資訊,請參閱 維基百科

Yii 實作了通用階層式 RBAC,遵循 NIST RBAC 模型。它透過 authManager 應用程式組件 提供 RBAC 功能。

使用 RBAC 涉及兩個部分的工作。第一部分是建立 RBAC 授權資料,第二部分是在需要的地方使用授權資料執行存取檢查。

為了方便我們接下來的描述,我們將首先介紹一些基本的 RBAC 概念。

基本概念

角色代表一組權限的集合(例如,建立文章、更新文章)。角色可以分配給一個或多個使用者。若要檢查使用者是否具有指定的權限,我們可以檢查使用者是否被分配了包含該權限的角色。

與每個角色或權限相關聯的,可能有一個規則。規則代表一段程式碼,將在存取檢查期間執行,以判斷對應的角色或權限是否適用於目前使用者。例如,「更新文章」權限可能有一個規則,檢查目前使用者是否為文章建立者。在存取檢查期間,如果使用者不是文章建立者,則會認為他/她不具有「更新文章」權限。

角色和權限都可以組織成階層結構。特別是,一個角色可能由其他角色或權限組成;而一個權限可能由其他權限組成。Yii 實作了部分排序階層結構,其中包括更特殊的樹狀階層結構。雖然角色可以包含權限,但反之則不成立。

配置 RBAC

在我們開始定義授權資料並執行存取檢查之前,我們需要配置 authManager 應用程式組件。Yii 提供了兩種型別的授權管理器:yii\rbac\PhpManageryii\rbac\DbManager。前者使用 PHP 腳本檔案來儲存授權資料,而後者將授權資料儲存在資料庫中。如果您的應用程式不需要非常動態的角色和權限管理,您可以考慮使用前者。

使用 PhpManager

以下程式碼顯示如何在應用程式配置中使用 yii\rbac\PhpManager 類別配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
        ],
        // ...
    ],
];

現在可以透過 \Yii::$app->authManager 存取 authManager

預設情況下,yii\rbac\PhpManager 將 RBAC 資料儲存在 @app/rbac 目錄下的檔案中。如果需要線上變更權限階層結構,請確保 Web 伺服器進程對該目錄及其中的所有檔案具有寫入權限。

使用 DbManager

以下程式碼顯示如何在應用程式配置中使用 yii\rbac\DbManager 類別配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\DbManager',
            // uncomment if you want to cache RBAC items hierarchy
            // 'cache' => 'cache',
        ],
        // ...
    ],
];

注意:如果您使用的是 yii2-basic-app 範本,則有一個 config/console.php 配置檔案,其中 authManager 除了 config/web.php 之外還需要額外宣告。如果是 yii2-advanced-app,則 authManager 應該只在 common/config/main.php 中宣告一次。

DbManager 使用四個資料庫表格來儲存其資料:

  • itemTable:用於儲存授權項目的表格。預設為「auth_item」。
  • itemChildTable:用於儲存授權項目階層結構的表格。預設為「auth_item_child」。
  • assignmentTable:用於儲存授權項目分配的表格。預設為「auth_assignment」。
  • ruleTable:用於儲存規則的表格。預設為「auth_rule」。

在您可以繼續之前,您需要在資料庫中建立這些表格。為此,您可以使用儲存在 @yii/rbac/migrations 中的遷移:

yii migrate --migrationPath=@yii/rbac/migrations

分隔遷移章節中閱讀更多關於使用不同命名空間遷移的資訊。

現在可以透過 \Yii::$app->authManager 存取 authManager

建立授權資料

建立授權資料的所有工作都與以下任務有關:

  • 定義角色和權限;
  • 建立角色和權限之間的關係;
  • 定義規則;
  • 將規則與角色和權限關聯;
  • 為使用者分配角色。

根據授權彈性需求,上述任務可以透過不同的方式完成。如果您的權限階層結構僅供開發人員變更,則可以使用遷移或命令列指令。遷移的優點是可以與其他遷移一起執行。命令列指令的優點是您可以在程式碼中很好地概述階層結構,而不是將其分散在多個遷移中。

無論哪種方式,最終您都將獲得以下 RBAC 階層結構:

Simple RBAC hierarchy

如果您需要動態形成權限階層結構,則需要 UI 或命令列指令。用於建立階層結構本身的 API 將不會有所不同。

使用遷移

您可以使用遷移透過 authManager 提供的 API 初始化和修改階層結構。

使用 ./yii migrate/create init_rbac 建立新的遷移,然後實作建立階層結構:

<?php
use yii\db\Migration;

class m170124_084304_init_rbac extends Migration
{
    public function up()
    {
        $auth = Yii::$app->authManager;

        // add "createPost" permission
        $createPost = $auth->createPermission('createPost');
        $createPost->description = 'Create a post';
        $auth->add($createPost);

        // add "updatePost" permission
        $updatePost = $auth->createPermission('updatePost');
        $updatePost->description = 'Update post';
        $auth->add($updatePost);

        // add "author" role and give this role the "createPost" permission
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // add "admin" role and give this role the "updatePost" permission
        // as well as the permissions of the "author" role
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
        // usually implemented in your User model.
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
    }
    
    public function down()
    {
        $auth = Yii::$app->authManager;

        $auth->removeAll();
    }
}

如果您不想硬編碼哪些使用者具有某些角色,請不要在遷移中放入 ->assign() 呼叫。相反,建立 UI 或命令列指令來管理分配。

可以使用 yii migrate 套用遷移。

使用命令列指令

如果您的權限階層結構完全不變,並且您有固定數量的使用者,您可以建立一個 -命令列指令,該指令將透過 authManager 提供的 API 一次初始化授權資料:

<?php
namespace app\commands;

use Yii;
use yii\console\Controller;

class RbacController extends Controller
{
    public function actionInit()
    {
        $auth = Yii::$app->authManager;
        $auth->removeAll();
        
        // add "createPost" permission
        $createPost = $auth->createPermission('createPost');
        $createPost->description = 'Create a post';
        $auth->add($createPost);

        // add "updatePost" permission
        $updatePost = $auth->createPermission('updatePost');
        $updatePost->description = 'Update post';
        $auth->add($updatePost);

        // add "author" role and give this role the "createPost" permission
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // add "admin" role and give this role the "updatePost" permission
        // as well as the permissions of the "author" role
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
        // usually implemented in your User model.
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
    }
}

注意:如果您使用的是進階範本,則需要將您的 RbacController 放在 console/controllers 目錄中,並將命名空間變更為 console\controllers

可以使用以下方式從命令列執行上述指令:

yii rbac/init

如果您不想硬編碼哪些使用者具有某些角色,請不要將 ->assign() 呼叫放入指令中。相反,建立 UI 或命令列指令來管理分配。

為使用者分配角色

作者可以建立文章,管理員可以更新文章並執行作者可以執行的所有操作。

如果您的應用程式允許使用者註冊,您需要在這些新使用者註冊後立即為他們分配角色。例如,為了讓所有註冊使用者在您的進階專案範本中成為作者,您需要修改 frontend\models\SignupForm::signup() 如下:

public function signup()
{
    if ($this->validate()) {
        $user = new User();
        $user->username = $this->username;
        $user->email = $this->email;
        $user->setPassword($this->password);
        $user->generateAuthKey();
        $user->save(false);

        // the following three lines were added:
        $auth = \Yii::$app->authManager;
        $authorRole = $auth->getRole('author');
        $auth->assign($authorRole, $user->getId());

        return $user;
    }

    return null;
}

對於需要具有動態更新授權資料的複雜存取控制的應用程式,可能需要使用 authManager 提供的 API 開發特殊的使用者介面(即管理面板)。

使用規則

如前所述,規則為角色和權限新增了額外的約束。規則是一個從 yii\rbac\Rule 擴展而來的類別。它必須實作 execute() 方法。在我們先前建立的階層結構中,作者無法編輯自己的文章。讓我們修正它。首先,我們需要一個規則來驗證使用者是否為文章作者:

namespace app\rbac;

use yii\rbac\Rule;
use app\models\Post;

/**
 * Checks if authorID matches user passed via params
 */
class AuthorRule extends Rule
{
    public $name = 'isAuthor';

    /**
     * @param string|int $user the user ID.
     * @param Item $item the role or permission that this rule is associated with
     * @param array $params parameters passed to ManagerInterface::checkAccess().
     * @return bool a value indicating whether the rule permits the role or permission it is associated with.
     */
    public function execute($user, $item, $params)
    {
        return isset($params['post']) ? $params['post']->createdBy == $user : false;
    }
}

上面的規則檢查 post 是否由 $user 建立。我們將在我們先前使用的指令中建立一個特殊的權限 updateOwnPost

$auth = Yii::$app->authManager;

// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);

// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);

// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);

// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);

現在我們得到了以下階層結構:

RBAC hierarchy with a rule

存取檢查

在授權資料準備就緒後,存取檢查就像呼叫 yii\rbac\ManagerInterface::checkAccess() 方法一樣簡單。由於大多數存取檢查都與目前使用者有關,為了方便起見,Yii 提供了一個捷徑方法 yii\web\User::can(),可以像下面這樣使用:

if (\Yii::$app->user->can('createPost')) {
    // create post
}

如果目前使用者是 Jane,ID=1,我們從 createPost 開始,並嘗試到達 Jane

Access check

為了檢查使用者是否可以更新文章,我們需要傳遞一個額外的參數,這是先前描述的 AuthorRule 所需的:

if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
    // update post
}

以下是目前使用者是 John 時發生的情況:

Access check

我們從 updatePost 開始,並經過 updateOwnPost。為了通過存取檢查,AuthorRule 應該從其 execute() 方法傳回 true。該方法從 can() 方法呼叫接收其 $params,因此值為 ['post' => $post]。如果一切順利,我們將到達分配給 John 的 author

對於 Jane 來說,情況稍微簡單一些,因為她是管理員:

Access check

在您的控制器中,有幾種方法可以實作授權。如果您想要細緻的權限,將新增和刪除的存取權限分開,那麼您需要檢查每個動作的存取權限。您可以將上述條件用於每個動作方法中,或使用 yii\filters\AccessControl

public function behaviors()
{
    return [
        'access' => [
            'class' => AccessControl::class,
            'rules' => [
                [
                    'allow' => true,
                    'actions' => ['index'],
                    'roles' => ['managePost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['view'],
                    'roles' => ['viewPost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['create'],
                    'roles' => ['createPost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['update'],
                    'roles' => ['updatePost'],
                ],
                [
                    'allow' => true,
                    'actions' => ['delete'],
                    'roles' => ['deletePost'],
                ],
            ],
        ],
    ];
}

如果所有 CRUD 操作都一起管理,那麼最好使用單一權限,例如 managePost,並在 yii\web\Controller::beforeAction() 中檢查它。

在上面的範例中,沒有參數與為存取動作指定的角色一起傳遞,但在 updatePost 權限的情況下,我們需要傳遞一個 post 參數才能使其正常運作。您可以透過在存取規則上指定 roleParams 將參數傳遞給 yii\web\User::can()

[
    'allow' => true,
    'actions' => ['update'],
    'roles' => ['updatePost'],
    'roleParams' => function() {
        return ['post' => Post::findOne(['id' => Yii::$app->request->get('id')])];
    },
],

在上面的範例中,roleParams 是一個閉包,它將在檢查存取規則時評估,因此模型只會在需要時載入。如果角色參數的建立是一個簡單的操作,您可以只指定一個陣列,如下所示:

[
    'allow' => true,
    'actions' => ['update'],
    'roles' => ['updatePost'],
    'roleParams' => ['postId' => Yii::$app->request->get('id')],
],

使用預設角色

預設角色是一個隱含地分配給所有使用者的角色。不需要呼叫 yii\rbac\ManagerInterface::assign(),並且授權資料不包含其分配資訊。

預設角色通常與規則相關聯,該規則決定角色是否適用於正在檢查的使用者。

預設角色通常用於已經具有某種角色分配的應用程式中。例如,應用程式可能在其使用者表格中具有「group」欄位,以表示每個使用者所屬的權限群組。如果每個權限群組都可以對應到 RBAC 角色,則可以使用預設角色功能自動將每個使用者分配到 RBAC 角色。讓我們使用一個範例來說明如何完成此操作。

假設在使用者表格中,您有一個 group 欄位,該欄位使用 1 來表示管理員群組,2 來表示作者群組。您計劃擁有兩個 RBAC 角色 adminauthor,分別表示這兩個群組的權限。您可以如下設定 RBAC 資料,首先建立一個類別:

namespace app\rbac;

use Yii;
use yii\rbac\Rule;

/**
 * Checks if user group matches
 */
class UserGroupRule extends Rule
{
    public $name = 'userGroup';

    public function execute($user, $item, $params)
    {
        if (!Yii::$app->user->isGuest) {
            $group = Yii::$app->user->identity->group;
            if ($item->name === 'admin') {
                return $group == 1;
            } elseif ($item->name === 'author') {
                return $group == 1 || $group == 2;
            }
        }
        return false;
    }
}

然後依照前一節中的說明建立您自己的指令/遷移:

$auth = Yii::$app->authManager;

$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);

$author = $auth->createRole('author');
$author->ruleName = $rule->name;
$auth->add($author);
// ... add permissions as children of $author ...

$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $author);
// ... add permissions as children of $admin ...

請注意,在上面,由於「author」被新增為「admin」的子角色,因此當您實作規則類別的 execute() 方法時,您也需要尊重此階層結構。這就是為什麼當角色名稱為「author」時,如果使用者群組為 1 或 2(表示使用者屬於「admin」群組或「author」群組),則 execute() 方法將傳回 true

接下來,透過在 yii\rbac\BaseManager::$defaultRoles 中列出這兩個角色來配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
            'defaultRoles' => ['admin', 'author'],
        ],
        // ...
    ],
];

現在,如果您執行存取檢查,adminauthor 這兩個角色都會透過評估與它們相關聯的規則來進行檢查。如果規則返回 true,則表示該角色適用於當前使用者。根據上述規則實作,這表示如果使用者的 group 值為 1,則 admin 角色將適用於該使用者;如果 group 值為 2,則 author 角色將適用。

發現錯字或您認為此頁面需要改進嗎?
在 github 上編輯 !